3-1 React15架构卡顿的原因
为什么页面会卡顿
要理解 React 15 的性能瓶颈,先需要了解页面流畅度与帧率(FPS)的关系:
| FPS | 体验 |
|---|---|
| ≥ 60 FPS | 非常流畅,肉眼几乎察觉不到延迟 |
| 30-60 FPS | 基本流畅,部分场景能感受到延迟 |
| < 30 FPS | 明显卡顿 |
60 FPS 意味着每秒钟渲染 60 帧画面,每一帧的可用时间为:
1000ms / 60 = 16.666ms
text
也就是说,React 必须在 16.67ms 内完成所有计算和 DOM 更新,用户才不会感知到卡顿。这 16.67ms 内需要完成的工作包括:
- JavaScript 逻辑执行(Diff 算法、状态计算等)
- DOM 操作(插入、更新、删除节点)
- 浏览器的样式计算和布局(Style & Layout)
- 浏览器的绘制(Paint)
- 浏览器的合成(Composite)
对于小型应用,React 15 在 16.67ms 内完全可以完成这些工作。但随着组件数量和页面状态的增加,Diff 算法的计算时间就会超出这个预算。
React 15 的架构:Stack Reconciler
React 15 的核心架构由两个模块组成:
Reconciler(协调器)
负责找出发生变化的组件。它采用的是栈式架构(Stack Reconciler)——一个同步的、不可中断的递归过程。
工作流程如同栈的"先进后出"特性:
setState 触发更新
↓
递归遍历组件树
↓
对每个组件执行 Diff 计算
↓
递归返回,收集所有变更
↓
一次性提交到真实 DOM
text
问题在于:一旦递归开始,就无法中断。 只有等整棵组件树遍历完毕、栈清空之后,才能进行后续工作。当组件树很大时,这个递归过程可能耗时几十甚至上百毫秒,远超 16.67ms 的帧预算。
Renderer(渲染器)
负责将协调器找出的变更渲染到页面上。React 15 中渲染器是同步阻塞的——协调器收集完所有变更后,渲染器一次性批量更新 DOM。
Stack Reconciler 的根本问题
用户输入 → JS 执行 Diff(阻塞 50ms) → DOM 更新 → 浏览器渲染
← 这段时间内用户感知到页面无响应 →
text
Stack Reconciler 的同步递归特性导致了以下问题:
| 问题 | 表现 |
|---|---|
| 无法中断 | 一旦开始 Diff,必须遍历完整棵树才能停止 |
| 无法优先级调度 | 所有更新一视同仁,动画和输入响应被阻塞 |
| 阻塞用户交互 | 在 Diff 期间,用户点击、输入等事件无法得到响应 |
| 帧预算超标 | 组件多时,单帧耗时远超 16.67ms |
一个直观的对比:同样的动画效果,React 15 版本明显出现卡顿,而 React 16 版本则非常流畅——切换到其他页面时,React 16 的动画进程会利用闲置时间继续更新状态,不会阻塞用户的交互操作。
Fiber:React 16 的新架构
React 团队针对 Stack Reconciler 的问题设计了全新的 Fiber 架构,带来了三个核心改进:
- 更快——通过可中断的异步渲染机制,避免长时间阻塞主线程
- 更智能——支持优先级调度,高优先级任务(如动画、用户输入)优先处理
- 更方便调试——Fiber 节点提供了更清晰的调试信息和错误追踪
Fiber 架构的核心思想是:把同步的递归遍历拆分成一系列可中断的小任务单元,每个任务单元就是一个 Fiber 对象。调度器可以根据优先级决定先执行哪些任务、暂停哪些任务,确保高优先级的用户交互永远不会被低优先级的计算阻塞。
下一节将深入 Fiber 对象的数据结构和工作原理。
↑